Periodic Subproblems

Here we show how multiple_run_period.py splits one hub object so that it can be a series of smaller hubs.

[ ]:
import copy

from pyehub import excel_to_request_format, pylp
from pyehub.energy_hub import EHubModel
from pyehub.energy_hub.input_data import InputData
from pyehub.energy_hub.utils import constraint

Constraint

The subproblems are kept as the same hub by ensuring the converter and storage capacities are the same across all hubs by applying these constraints.

[ ]:
@constraint()
def same_converter_constraint(converter, hubs):
    """
    Constraint to ensure the capacities are kept constant across all the subproblems.
    """
    for i in range(len(hubs) - 1):
        yield hubs[i].capacities[converter] == hubs[i + 1].capacities[converter]


@constraint()
def same_storage_constraint(storage, hubs):
    """
    Constraint to ensure the capacities are kept constant across all the subproblems.
    """
    # TODO: Check that storage capacites are also kept the same
    for i in range(len(hubs) - 1):
        yield hubs[i].storage_capacity[storage] == hubs[i + 1].storage_capacity[storage]

Splitting the hubs

All the hub information in the subproblems are the same but the stream data is split depending on the given parameters: - The number of periods dictates the number of subproblems the original hub is being broken into. - The length of the periods determines the nubmer of timesteps included in each subproblem. - The number of periods in each sample period determines how many periods are in a sample period represented by a single period. - The sample period position determines in the number of periods in each sample period the represenative period.

An Example:

A hub with a year of houlry data to be solved with the second week representing each month in the year. There would be: - Number of periods = 12 - Length of each period = 168 (hours in a week) - Number of periods in each sample period = 4 (4 weeks per month) - Sample period position = 2

[ ]:
def split_hubs(
    excel=None,
    request=None,
    max_carbon=None,
    num_periods=1,
    len_periods=24,
    num_periods_in_sample_period=1,
    sample_period_position=0,
):
    """
    Splits a PyEHub into a series of smaller hubs with a given period.
    """
    if excel:
        request = excel_to_request_format.convert(excel)

    if request:
        _data = InputData(request)
    else:
        raise RuntimeError("Can't create hubs with no data.")

    hubs = []

    if (num_periods * len_periods * num_periods_in_sample_period) > len(
        request["time_series"][0]["data"]
    ):
        raise IndexError("Not enough data to cover all the periods.")

    if num_periods_in_sample_period <= sample_period_position:
        raise IndexError("Not enough periods in sample to start at the given position.")

    for i in range(0, num_periods):
        temp_request = copy.deepcopy(request)
        for stream in temp_request["time_series"]:
            stream["data"] = stream["data"][
                len_periods
                * (
                    i + i * (num_periods_in_sample_period - 1) + sample_period_position
                ) : len_periods
                * (
                    i
                    + 1
                    + i * (num_periods_in_sample_period - 1)
                    + sample_period_position
                )
            ]
        hub = EHubModel(request=temp_request)
        hubs.append(hub)
    return hubs

Combining constraint

[ ]:
def merge_hubs(hubs):
    """
    Compiles and combines the constraints of each subproblem into one list.
    :param hubs: List of EHubs.
    :return: The list of constraints for each hub combined with the capacities constraint to ensure the same converter capacities across all EHubs.
    """
    constraints = []
    for hub in hubs:
        hub.recompile()
        for constr in hub.constraints:
            constraints.append(constr)

    for converter in hubs[0].technologies:
        for c in same_converter_constraint(converter, hubs):
            constraints.append(c)

    for storage in hubs[0].storages:
        for c in same_storage_constraint(storage, hubs):
            constraints.append(c)
    return constraints

Main run function

The hub is turned into a list of the seperate hub problems. All the constraints for each hub aswell as the constraints tying each hub together are gathered into a list. The investement_cost from one hub (as the investement_costs for all the hubs will be same as the same amount of each converter and storage is installed), and the oeprating and maintence costs from all the hubs are combined into the objective for the solver. the status of the solver and the list of all the hubs is returned after it is solved.

[ ]:
def run_split_period(
    excel=None,
    request=None,
    output_filename=None,
    max_carbon=None,
    num_periods=1,
    len_periods=24,
    num_periods_in_sample_period=1,
    sample_period_position=0,
    solver="glpk",
):
    """
    Core function for splitting a PyEHub model into smaller problems to solve together.
    :param excel: Input excel file if the hub to be split is in excel. Converted into request format before being split into subproblems.
    :param request: Input in request JSON format if the hub to be split is in JSON.
    :param output_filename: Name for file to right the output to if an output file is being used.
    :param max_carbon: Max carbon value if using a capped carbon value.
    :param num_periods: Number of sub problem EHubs to be solved together.
    :param len_periods: Number of time steps per sub problem EHub to be solved.
    :param num_periods_in_sample_period: Number of periods being grouped together to be represented by 1 sub problem EHub. Example: One week representing a whole month would be ~four periods in a sample period.
    :param sample_period_position: Which period in the grouped sample to use as the representative EHub. Example the second week of every month would be two.
    :param solver: Which MILP solver to use.
    """
    hubs = split_hubs(
        excel,
        request,
        max_carbon,
        num_periods,
        len_periods,
        num_periods_in_sample_period,
        sample_period_position,
    )
    constraints = merge_hubs(hubs)

    objective = hubs[0].investment_cost
    for hub in hubs:
        objective += hub.operating_cost + hub.maintenance_cost

    status = pylp.solve(
        objective=objective, constraints=constraints, minimize=True, solver=solver
    )

    return status, hubs

    # TODO: Figure out setting up the output

Solution

Each hub has its own solution dictionary. Taking the operating and maintence cost (multipled by the number of periods they are supposed to represent) from each hub and combining it with the invesetment cost returns the total cost for the original larger problem.

[ ]:
n = 2

status, results = run_split_period(
    excel="two_day_test_file.xlsx",
    num_periods=1,
    len_periods=24,
    num_periods_in_sample_period=n,
    solver="glpk",
)

absolute_cost = results[0].solution_dict["investment_cost"]
for result in results:
    absolute_cost += (
        result.solution_dict["maintenance_cost"] * n
        + result.solution_dict["operating_cost"] * n
    )
print(absolute_cost)

Running the model normally

To demonstrate the accuracy of the subproblem modeling this example hub is run normally.

[ ]:
hub = EHubModel(excel="two_day_test_file.xlsx")
results = hub.solve()
results["solution"]["total_cost"]